package cn.echoparrot.application;

import cn.echoparrot.domain.entity.Dataset;
import cn.echoparrot.domain.entity.FileInfo;
import cn.echoparrot.domain.service.DatasetService;
import cn.echoparrot.domain.service.Pipeline;
import cn.echoparrot.domain.vo.NewDataset;
import cn.echoparrot.repository.neo4j.DatasetMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * 知识库管理相关的API
 * 遵循RESTful风格，路径尽量只用名词，不用动词，对于复数资源使用’s‘尾缀。
 * 文件是操作的基础单位，文件切片在这个系统里被视作为内部细节，不暴露给用户。
 * 如何让大模型对语义理解得更准确，首先是程序员需要考虑的，而非对用户的要求。
 *
 * 1. 对知识库的操作：创建、删除、修改，列出；
 * 2. 对文件的操作：上传文件，删除文件，列出文件；
 * 注意：暂时不提供文件下载，文件修改，但可以直接重新上传同名文件覆盖。
 *
 */
@RestController
@RequestMapping("/datasets")
public class DatasetController {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final DatasetService datasetService;
    private final DatasetMapper datasetMapper;
    private final Pipeline pipeline;
    private static final int FILE_MAX_SIZE = 10 * 1024 * 1024; // 最大文件大小为10M
    @Autowired
    private EmbeddingModel embeddingModel;

    @Autowired
    public DatasetController(DatasetService datasetService, Pipeline pipeline, DatasetMapper datasetMapper) {
        this.datasetService = datasetService;
        this.pipeline = pipeline;
        this.datasetMapper = datasetMapper;
    }

    /**
     * 创建一个新的知识库
     *
     * @param dataset 知识库的名称，可以是中文名称。
     * @return
     */
    @PostMapping("/")
    public Dataset create(@RequestBody NewDataset dataset) {
        return datasetService.create(dataset,0,1024);
    }

    /**
     * 更新知识库名称或描述
     * 资源通过 URL 路径中的 ID 唯一标识，服务端可通过该 ID 直接定位资源。
     * 旧数据已存储在服务端，客户端只需传递新的数据即可完成更新。
     * @param datasetId 原知识库名称
     * @param dataset     新知识库的名称或描述
     */
    @PutMapping("/{datasetId}")
    public Dataset update(@PathVariable String datasetId, @RequestBody Dataset dataset) {
        Assert.hasText(datasetId, "知识库唯一标识不能为空！");
        Assert.notNull(dataset, "知识库对象不能为空！");
        Assert.hasText(dataset.name(), "知识库名称不能为空！");
        Assert.isTrue(dataset.id().equals(datasetId), "知识库标识不一致！");
        return datasetService.update(dataset);
    }

    /**
     * 删除一个知识库
     * RESTful风格，使用DELETE方法只需要传递要删除的资源的唯一标识。
     * @param datasetId
     */
    @DeleteMapping("/{datasetId}")
    public int delete(@PathVariable Long datasetId) throws IOException {
        Assert.state(datasetId>=0, "datasetId不能为空");
        int deleted = datasetService.delete(datasetId);
        return deleted;
    }

    /**
     * 列出所有的知识库名称
     * 短期内知识库数量不会太多，所以使用List返回，暂时不考虑分页处理。
     *
     * @return 知识库名称列表
     */
    @GetMapping("/")
    public List<Dataset> list() {
        List<Dataset> datasets = datasetService.listDatasets();
        return datasets;
    }

    /**
     * 上传原始文件。
     * 由于上传的文件可能比较大，而对文件的分析处理也可能比较耗时。
     * 需要尽快响应客户端，只要文件落地持久化存储后就应该立即响应。
     * 其后的对文件的分析处理。都通过另外的线程异步处理（也可以并行处理）。
     * 文件状态的更新不放在Session里，因为我们会尽量减少Session存在的必要性，但可以与一个Batch号挂勾。
     * URL里':'是作为路径分隔符，不能作为文件名。但neo4j的elementId有':'。前端需要对datasetId里的冒号编码为 %3A
     *
     * @param files       上传的文件
     * @param datasetId 知识库名字
     * @throws Exception
     */
    @PostMapping("/files/{datasetId}")
    public long upload(@RequestPart MultipartFile[] files, @PathVariable Long datasetId) throws Exception {
        Assert.state(datasetId>=0, "datasetId不能为空");
        Assert.notEmpty(files, "没有上传文件！");
        Optional<Dataset> datasetOptional =datasetMapper.findById(datasetId);
        if(datasetOptional.isEmpty()){
            throw  new IllegalArgumentException("知识库不存在！");
        }
        Dataset dataset = datasetOptional.get();
        // 检查文件的大小
        // 检查文件的格式，通过文件后缀名；
        List<Path> paths = Arrays.stream(files).filter(file -> file.getSize() > 0 && file.getSize() < FILE_MAX_SIZE)
                .filter(file -> datasetService.isSupportive(StringUtils.getFilenameExtension(file.getOriginalFilename())))
                .map(file -> {
                    // 保存符合要求的文件落盘到本地存储。
                    Path path = datasetService.uploadPath(dataset.userId(), dataset.name(), file.getOriginalFilename());
                    try {
                        file.transferTo(path);
                    } catch (IllegalStateException | IOException e) {
                        logger.error("知识库：{}, 文件：{}，无法保存至本地: ", datasetId, file.getOriginalFilename(), e);
                    }
                    return path;
                }).toList();

        long batch = pipeline.process(datasetId,embeddingModel,paths);
        return batch;
    }

    /**
     * 列出知识库已经上传过的所有的文件。
     *
     * @param datasetName 知识库名字
     * @return 所有上传的文件。返回的信息包括：文件名、文件大小、上传时间。
     */
    @GetMapping("/files/{datasetName}")
    public List<FileInfo> listFiles(@PathVariable String datasetName) throws IOException {
        Integer userId = 0;
        return datasetService.listUploads(userId,datasetName);
    }

    /**
     * 删除一个或多个文件。
     *
     * @param datasetId 知识库ID，注意不是知识库名称，ID里有特殊字符需要进行URL encode。
     * @param filenames 文件名，注意不要写成路径形式，避免与URL命名冲突。
     */
    @DeleteMapping("/files/{datasetId}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteFiles(@PathVariable Long datasetId, @RequestBody String[] filenames) throws IOException {

        Assert.state(datasetId>=0, "datasetId cannot be empty");
        Assert.notEmpty(filenames, "filenames cannot be empty");
        datasetService.delete(datasetId,filenames);
    }

    /**
     * 通过SSE告诉用户，哪些文件已经处理过了，哪些文件正在处理中。
     * SseEmitter对象在发起sse请求时创建，从Flux热流中订阅。
     *
     * 如果在发起sse请求时，找到Flux就正常操作，找不到那无论是根本不存batch批次号或者已完成，都发出complete。
     * 以时间戳为批次号。既是限制过高的并发，也没有必要生成一个128位的UUID转换为Hex形式.
     *
     */
    @GetMapping("/sse/{batch}")
    public ResponseEntity<SseEmitter> streamProcessingStatus(@PathVariable Long batch) {
        SseEmitter emitter = new SseEmitter(600_000L);
        Flux<Pipeline.Result> flux = pipeline.findBatch(batch);
        if(flux ==  null){
            logger.info("batch: {} not found",batch);
            emitter.complete();
        }else{
            flux.subscribe(result -> {
                logger.info("result: {}",result);
                try {
                    emitter.send(SseEmitter.event().name(result.success() ? "success" : "failure").data(result.name()));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }, throwable -> {
                logger.error("error: {}",throwable);
                emitter.completeWithError(throwable);
            }, () -> {
                logger.info("complete");
                emitter.complete();
            });
        }

        return ResponseEntity.ok()
                .contentType(new MediaType(MediaType.TEXT_EVENT_STREAM, StandardCharsets.UTF_8))
                .body(emitter);
    }
}
